<template>
  <view class="tn-image-upload">
    <view class="tn-image-upload__container">
      <!-- 文件列表 -->
      <slot name="list" :file="fileList">
        <view
          v-for="(item, index) in fileLists"
          :key="index"
          class="tn-image-upload__item tn-image-upload__item--file"
        >
          <!-- 删除按钮 -->
          <view v-if="showDelete" class="tn-image-upload__delete" @tap.stop="deleteItem(index)">
            <view class="tn-image-upload__icon">
              <tn-icon icon="close-circle"></tn-icon>
            </view>
          </view>
          <!-- 进度条 -->
          <view v-if="showProgress" class="tn-image-upload__progress" :style="{width: `${item?.progress || 0}%`}"></view>
          <!-- 失败重试 -->
          <view v-if="item.error" class="tn-image-upload__retry" @tap.stop="retryUpload(index)">
            <view class="tn-image-upload__icon">
              <tn-icon icon="circle-arrow"></tn-icon>
            </view>
          </view>
          <!-- 图片 -->
          <view class="tn-image-upload__image">
            <image
              :src="item.url"
            ></image>
          </view>
          
        </view>
      </slot>

      <!-- 添加按钮 -->
      <slot name="add">
        <view
          v-if="fileList.length < limit"
          class="tn-image-upload__item tn-image-upload__item--add"
          @tap.stop="selectFile"
        >
          <view class="tn-image-upload__icon">
            <tn-icon icon="add-circle"></tn-icon>
          </view>
        </view>
      </slot>
    </view>
  </view>
</template>

<script lang="ts" setup>
import { imageUploadProps, imageUploadEmits, ImageUploadList, ImageUploadUserFile } from '../../../types/tn-image-upload'
import { ref, watch } from 'vue'
import { jsonString, loading, closeLoading, modal } from '../../../utils'
import TnIcon from '../../tn-icon/src/Icon.vue'

const props = defineProps(imageUploadProps)
const emits = defineEmits(imageUploadEmits)

// 文件列表
let fileLists = ref<ImageUploadList[]>([])
// 标记文件是否正在上传
let uploading: boolean = false
// 如果用户设置了已上传文件列表更新文件列表数据
watch(() => props.fileList, (val: ImageUploadUserFile[]) => {
  // 首先检查内部是否已经添加过这张图片，因为外部绑定了一个对象给fileList的话(对象引用)，进行修改外部fileList时，
  // 会触发watch，导致重新把原来的图片再次添加到内部文件列表
  val.map(file => {
    const hasInner = fileLists.value.some(innerFile => innerFile.url === file.url)
    !hasInner && fileLists.value.push({
      url: file.url,
      error: false,
      progress: 100
    })
  })
}, {
  immediate: true
})
watch(fileLists.value, (val: ImageUploadList[]) => {
  emits('list-change', val, props.index)
})

// 文件选择
const selectFile = () => {
  if (props.disabled) return

  // 当前文件列表长度
  let currentFileListLength = fileLists.value.length
  // 最大允许选择的文件数量
  const maxSelectCount = props.limit - currentFileListLength
  const chooseFile = new Promise<UniApp.ChooseImageSuccessCallbackResult>((resolve, reject) => {
    uni.chooseImage({
      count: props?.multiple ? maxSelectCount : 1,
      sourceType: props.sourceType,
      sizeType: props.sizeType,
      success: resolve,
      fail: reject
    })
  })

  chooseFile.then((res: UniApp.ChooseImageSuccessCallbackResult) => {
    if (Array.isArray(res.tempFiles)) {
      const beforeAddFilelistLength = currentFileListLength
      res.tempFiles.map((file, index) => {
        // 判断选择的文件是否合法
        if (!_checkFileExt(file)) return
        // 只允许单选只验证第一个
        if (!props.multiple && index >= 1) return
        if (file.size > props.maxSize) {
          emits('oversize', file, props.index)
          _showToast('文件大小可允许值')
          return
        }
        currentFileListLength = fileLists.value.length
        if (currentFileListLength >= props.limit) {
          emits('exceed', props.index)
          _showToast('超出最大允许文件数')
          return
        }

        fileLists.value.push({
          url: (file as UniApp.ChooseImageSuccessCallbackResultFile).path,
          progress: 0,
          error: false,
          file: file
        })
      })
      emits('choose-complete', fileLists.value, props.index)
      if (props.autoUpload) uploadFile(beforeAddFilelistLength)
    }
  }).catch(err => {
    emits('choose-fail', err, props.index)
  })
}

// 上传文件
const uploadFile = async (startIndex: number = 0): Promise<void> => {
  if (props.disabled || uploading) return

  if (!props.action) return console.error('[tn-image-upload]请填写action地址')
  
  // 文件全部上传完毕
  if (startIndex >= fileLists.value.length) {
    emits('uploaded', fileLists.value, props.index)
    return
  }
  // 如果当前上传完毕自动上传下一张
  if (fileLists.value[startIndex].progress === 100) {
    fileLists.value[startIndex].task = null
    if (props.autoUpload) uploadFile(startIndex + 1)
    return
  }

  // 执行beforeUpload钩子函数
  if (props.beforeUpload && typeof(props.beforeUpload) === 'function') {
    let beforeUpload = props.beforeUpload(fileLists.value[startIndex], props.index)

    if (beforeUpload.then && typeof(beforeUpload.then) === 'function') {
      // Promise函数
      await beforeUpload.then(() => {
        return uploadFile(startIndex + 1)
      }).catch(() => {})
    } else if (beforeUpload === true) {
      // 普通函数
      return uploadFile(startIndex + 1)
    } else {
      return uploadFile(startIndex + 1)
    }
  }

  // 开始执行上传操作
  fileLists.value[startIndex].error = false
  uploading = true
  loading({ title: '正在上传' })
  // 创建上传对象
  const task = uni.uploadFile({
    url: props.action,
    filePath: fileLists.value[startIndex].url,
    name: props.name,
    formData: props.formData,
    header: props.header,
    success: (res) => {
      // 判断服务器返回的是否为json字符串
      const data = jsonString(res.data) ? JSON.parse(res.data) : res.data
      if (![200, 201, 204].includes(res.statusCode)) {
        handleUploadError(startIndex, data)
      } else {
        fileLists.value[startIndex].data = data
        fileLists.value[startIndex].progress = 100
        fileLists.value[startIndex].error = false
        emits('success', data, fileLists.value[startIndex], fileLists.value, props.index)
      }
    },
    fail: (err) => {
      handleUploadError(startIndex, err)
    },
    complete: (res) => {
      closeLoading()
      uploading = false
      uploadFile(startIndex + 1)
      emits('change', fileLists.value[startIndex], fileLists.value, props.index)
    }
  })
  fileLists.value[startIndex].task = task
  task.onProgressUpdate(res => {
    if (res.progress > 0) {
      fileLists.value[startIndex].progress = res.progress
      emits('progress', res, fileLists.value, props.index)
    }
  })
}
// 重新上传图片
const retryUpload = (index: number) => {
  if (props.disabled) return
  fileLists.value[index].progress = 0
  fileLists.value[index].error = false
  fileLists.value[index].data = null
  uploadFile(index)
}

// 处理文件删除事件
const deleteItem = (index: number) => {
  if (!props.showDelete || props.disabled) return
  modal({
    title: '提示',
    content: '确定需要删除吗?',
    showCancel: true,
  }).then(async (res) => {
    if (res) {
      // 点击了确认
      // 执行移除前的钩子函数
      if (props.beforeRemove && typeof(props.beforeRemove) === 'function') {
        let beforeRemove = props.beforeRemove(fileLists.value[index], props.index)
        if (beforeRemove.then && typeof(beforeRemove.then) === 'function') {
          // Promise函数
          await beforeRemove.then(() => {
            _removeFile(index)
          }).catch(() => {})
        } else if (beforeRemove === true) {
          _removeFile(index)
        }
      } else {
        _removeFile(index)
      }
    }
  }).catch(() => {})
}

// 点击预览图片
const handlePreviewImage = (index: number) => {
  // 获取当前点击的图片信息
  const currentFile = fileLists.value[index]
  if (!currentFile) return
  const images: string[] = fileLists.value.map(item => item.url || '').filter(item => item !== '')
  uni.previewImage({
    urls: images,
    current: currentFile.url,
    success: () => {
      emits('preview', currentFile, fileLists.value, props.index)
    },
    fail: () => {
      _showToast('预览图片失败')
    }
  })
}

// 处理上传发生错误的情况
const handleUploadError = (index: number, err: any) => {
  fileLists.value[index].progress = 0
  fileLists.value[index].error = true
  fileLists.value[index].data = null
  _showToast('上传失败, 请稍后尝试')
  emits('error', err, fileLists.value[index], fileLists.value, props.index)
}
// 删除文件
const _removeFile = (index: number) => {
  const fileInfo = fileLists.value[index]
  if (!fileInfo) return
  // 如果文件正在上传首先终止任务再进行删除
  if (fileInfo.progress && fileInfo.progress < 100 && fileInfo.progress > 0) {
    typeof fileInfo.task !== 'undefined' && fileInfo?.task?.abort()
  }
  fileLists.value.splice(index, 1)
  emits('remove', fileInfo, fileLists.value, props.index)
  _showToast('删除成功')
}
// 检查文件是否符合限制
const _checkFileExt = (file: UniApp.ChooseImageSuccessCallbackResultFile | File) => {
  // 文件后缀名
  let fileExt = ''
  const reg = /.+\./

  // #ifdef H5
  fileExt = (file as File).name.replace(reg, '').toLowerCase()
  // #endif
  // #ifndef H5
  fileExt = (file as UniApp.ChooseImageSuccessCallbackResultFile).path.replace(reg, '').toLowerCase()
  // #endif
  const result = props.limitType.some(ext => ext.toLowerCase() === fileExt)
  if (!result) _showToast(`不支持${fileExt}格式的文件`)
  return result
}
// 内部提示信息
const _showToast = (msg: string) => {
  if (!props?.showTips) return

  uni.showToast({
    icon: 'none',
    title: msg
  })
}

// 外部操作函数
// 清除列表
const clear = () => {
  fileLists.value = []
}
// 开始上传文件(重新上传失败的文件)
const upload = () => {
  uploadFile(0)
}


defineExpose({
  /** 清空列表 */
  clear,
  /** 手动上传 / 重新上传失败的文件 */
  upload
})
</script>

<script lang="ts">
export default {
  options: {
    // 在微信小程序中将组件节点渲染为虚拟节点，更加接近Vue组件的表现(不会出现shadow节点下再去创建元素)
    virtualHost: true
  }
}
</script>

<style lang="scss" scoped>
@import '../../../theme.scss';

.tn-image-upload {
  position: relative;
  width: 100%;

  &__container {
    width: 100%;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
  }

  &__item {
    width: 28%;
    height: 0;
    padding-bottom: 28%;
    box-sizing: border-box;
    position: relative;
    border-radius: 10rpx;
    overflow: hidden;
    margin: 16rpx;
    margin-left: 0rpx;
    margin-bottom: 0rpx;

    &--add {
      background-color: #E6E6E6;
      color: #FFFFFF;
      font-size: 80rpx;
      .tn-image-upload__icon {
        line-height: 1;
        position: absolute;
        left: 50%;
        top: 50%;
        transform: translate(-50%, -50%);
      }
    }
  }

  &__image {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    image {
      width: 100%;
      height: 100%;
    }
  }

  &__delete {
    position: absolute;
    z-index: 10;
    top: 0;
    right: 0;
    width: 0;
    height: 0;
    border-top: 60rpx solid;
    border-left: 60rpx solid transparent;
    border-top-color: map-get(map-get($tn-type-color-map, 'danger'), 'normal');
    // border-color: map-get(map-get($tn-type-color-map, 'danger'), 'normal') transparent transparent;
    .tn-image-upload__icon {
      position: absolute;
      color: #FFFFFF;
      right: 6rpx;
      top: -52rpx;
      font-size: 26rpx;
      line-height: 1;
    }
  }

  &__retry {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.08);
    z-index: 3;
    .tn-image-upload__icon {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: #FFFFFF;
      font-size: 80rpx;
      width: 80rpx;
      height: 80rpx;
      font-weight: 600;
      line-height: 1;
      transform-origin: center center;
      animation: retry-animation 4s ease-out 4s infinite normal none;
    }
  }

  &__progress {
    position: absolute;
    bottom: 0;
    left: 0;
    width: 100%;
    height: 10rpx;
    background-color: $tn-main-color;
    z-index: 2;
    transform-origin: left center;
    border-radius: 20rpx;
  }
}

@keyframes retry-animation {
  0% {
    transform: translate(-50%, -50%) rotate(0deg);
  }
  50% {
    transform: translate(-50%, -50%) rotate(360deg);
  }
  100% {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}
</style>
